/*
 * Copyright 2011 Daniel Krieg
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.code.csf.locators.ejb;

import com.google.code.csf.ServiceLocator;
import com.google.code.csf.locators.JndiObjectLocator;

import javax.naming.NamingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Properties;

public class LocalSlsbServiceLocator<ServiceType> extends JndiObjectLocator implements ServiceLocator<ServiceType> {
    private Class ejbHome;
    private boolean cacheHome = true;
    private Object cachedHome;
    private Method createMethod;
    private final Object homeMonitor = new Object();
    private volatile boolean homeAsComponent = false;
    private boolean homeRefreshable;

    public LocalSlsbServiceLocator() {
    }

    public LocalSlsbServiceLocator(Properties environment) {
        super(environment);
    }

    public ServiceType locate(Class toLocate) throws Throwable {
        return getSessionBeanInstance();
    }

    public void release(ServiceType toRelease) {
        removeSessionBeanInstance(toRelease);
    }

    public final void setHomeRefreshable(boolean homeRefreshable) {
        this.homeRefreshable = homeRefreshable;
    }

    public final void setCacheHome(boolean cacheHome) {
        this.cacheHome = cacheHome;
    }

    /**
     * Return an EJB component instance to delegate the call to.
     * <p>The default implementation delegates to {@link #newSessionBeanInstance}.
     * @return the EJB component instance
     * @throws javax.naming.NamingException if thrown by JNDI
     * @throws java.lang.reflect.InvocationTargetException if thrown by the create method
     * @see #newSessionBeanInstance
     */
    protected ServiceType getSessionBeanInstance() throws NamingException, InvocationTargetException {
        return newSessionBeanInstance();
    }

    /**
     * Remove the given EJB instance.
     * To be invoked by concrete remote SLSB invoker subclasses.
     *
     * @param ejb the EJB instance to remove
     */
    protected void removeSessionBeanInstance(ServiceType ejb) {
        if (ejb != null && !this.homeAsComponent) {
            try {
                ejb.getClass().getMethod("remove").invoke(ejb);
            } catch (Throwable ex) {
                //  ignore
            }
        }
    }

    /**
     * Return a new instance of the stateless session bean.
     * To be invoked by concrete remote SLSB invoker subclasses.
     * <p>Can be overridden to change the algorithm.
     *
     * @return a new EJBObject or EJBLocalObject
     * @throws NamingException if thrown by JNDI
     * @throws java.lang.reflect.InvocationTargetException
     *                         if thrown by the create method
     * @see #create
     */
    protected ServiceType newSessionBeanInstance() throws NamingException, InvocationTargetException {
        return create();
    }

    /**
     * Refresh the cached home object, if applicable.
     * Also caches the create method on the home object.
     *
     * @throws NamingException if thrown by the JNDI lookup
     * @see #getCreateMethod
     */
    protected void refreshHome() throws NamingException {
        synchronized (homeMonitor) {
            Object home = lookup();
            if (cacheHome) {
                cachedHome = home;
                createMethod = getCreateMethod(home);
            }
        }
    }

    /**
     * Determine the create method of the given EJB home object.
     *
     * @param home the EJB home object
     * @return the create method
     * @throws RuntimeException if the method couldn't be retrieved
     */
    protected Method getCreateMethod(Object home) throws RuntimeException {
        if (homeAsComponent) {
            return null;
        }
        if (!isHomeClass(home)) {
            // An EJB3 Session Bean...
            homeAsComponent = true;
            return null;
        }

        try {
            // Cache the EJB create() method that must be declared on the home interface.
            return home.getClass().getMethod("create");
        } catch (NoSuchMethodException ex) {
            throw new RuntimeException("EJB home [" + home + "] has no no-arg create() method");
        }
    }

    protected Class<?> getHomeClass() throws ClassNotFoundException {
        return Class.forName("javax.ejb.EJBLocalHome");
    }

    /**
     * Return the EJB home object to use. Called for each invocation.
     * <p>Default implementation returns the home created on initialization,
     * if any; else, it invokes lookup to get a new proxy for each invocation.
     * <p>Can be overridden in subclasses, for example to cache a home object
     * for a given amount of time before recreating it, or to test the home
     * object whether it is still alive.
     *
     * @return the EJB home object to use for an invocation
     * @throws javax.naming.NamingException if proxy creation failed
     * @see #lookup
     * @see #getCreateMethod
     */
    protected Object getHome() throws NamingException {
        if (!cacheHome || (!homeRefreshable)) {
            return (cachedHome != null ? cachedHome : lookup());
        } else {
            synchronized (homeMonitor) {
                if (cachedHome == null) {
                    cachedHome = lookup();
                    createMethod = getCreateMethod(cachedHome);
                }
                return cachedHome;
            }
        }
    }

    /**
     * Invokes the <code>create()</code> method on the cached EJB home object.
     *
     * @return a new EJBObject or EJBLocalObject
     * @throws NamingException if thrown by JNDI
     * @throws java.lang.reflect.InvocationTargetException
     *                         if thrown by the create method
     */
    @SuppressWarnings("unchecked")
    protected ServiceType create() throws NamingException, InvocationTargetException {
        try {
            Object home = getHome();
            Method createMethodToUse = createMethod;
            if (createMethodToUse == null) {
                createMethodToUse = getCreateMethod(home);
            }
            if (createMethodToUse == null) {
                return (ServiceType) home;
            }
            // Invoke create() method on EJB home object.
            return (ServiceType) createMethodToUse.invoke(home);
        } catch (IllegalAccessException ex) {
            throw new RuntimeException("Could not access EJB home create() method", ex);
        }
    }

    @SuppressWarnings("unchecked")
    private boolean isHomeClass(Object home) {
        if(ejbHome == null) {
            try {
                ejbHome = getHomeClass();
            } catch (ClassNotFoundException e) {
                throw new RuntimeException(e);
            }
        }
        return (ejbHome.isAssignableFrom(home.getClass()));
    }
}
